package org.sergeys.library.swing.treetable;

// modified by svs

/*
 * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistribution in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials
 *   provided with the distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
 * EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY
 * DAMAGES OR LIABILITIES SUFFERED BY LICENSEE AS A RESULT OF OR
 * RELATING TO USE, MODIFICATION OR DISTRIBUTION OF THIS SOFTWARE OR
 * ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE
 * FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT,
 * SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF
 * THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS
 * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed, licensed or
 * intended for use in the design, construction, operation or
 * maintenance of any nuclear facility.
 */

import java.awt.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import javax.swing.table.*;

import java.awt.event.*;

import java.util.EventObject;

/**
 * This example shows how to create a simple JTreeTable component, by using a
 * JTree as a renderer (and editor) for the cells in a particular column in the
 * JTable.
 *
 * @version 1.2 10/27/98
 *
 * @author Philip Milne
 * @author Scott Violet
 */
public class JTreeTable extends JTable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    /** A subclass of JTree. */
    protected TreeTableCellRenderer tree;

    public JTreeTable(final TreeTableModel treeTableModel) {
        super();

        // Creates the tree. It will be used as a renderer and editor.
        tree = new TreeTableCellRenderer(treeTableModel);

        // Installs a tableModel representing the visible rows in the tree.
        TreeTableModelAdapter adapter = new TreeTableModelAdapter(
                treeTableModel, tree);
        super.setModel(adapter);

        for (int i = 0; i < treeTableModel.getColumnCount(); i++) {
            if (treeTableModel.getColumnWidth(i) != 0) {
                getColumnModel().getColumn(i).setPreferredWidth(
                        treeTableModel.getColumnWidth(i));
            }
        }

        // Forces the JTable and JTree to share their row selection models.
        ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper();
        // svs: single selection
        selectionWrapper
                .setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        selectionWrapper.getListSelectionModel().setSelectionMode(
                ListSelectionModel.SINGLE_SELECTION);

        tree.setSelectionModel(selectionWrapper);
        setSelectionModel(selectionWrapper.getListSelectionModel());

        // Installs the tree editor renderer and editor.
        setDefaultRenderer(TreeTableModel.class, tree);
        // svs: no editor
        // setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor());

        // No grid.
        setShowGrid(false);

        // No intercell spacing
        setIntercellSpacing(new Dimension(0, 0));

        // And update the height of the trees row to match that of
        // the table.
        if (tree.getRowHeight() < 1) {
            // Metal looks better like this.
            setRowHeight(20);
        }

        // TODO: findbugs says it's not needed

        // this.addMouseListener(new MouseAdapter(){
        //
        // @Override
        // public void mouseClicked(MouseEvent e) {
        //
        // super.mouseClicked(e);
        // }
        // });
    }

    @Override
    protected void processMouseEvent(MouseEvent e) {

        // super.processMouseEvent(e);
        boolean processed = false;

        // svs: pass mouse event to the tree, instead of editor
        for (int counter = getColumnCount() - 1; counter >= 0; counter--) {
            if (getColumnClass(counter) == TreeTableModel.class) {

                Rectangle cell = getCellRect(0, counter, true);
                if (e.getX() >= cell.x && e.getX() <= (cell.x + cell.width)) {

                    MouseEvent newME = new MouseEvent(JTreeTable.this.tree,
                            e.getID(), e.getWhen(), e.getModifiers(), e.getX()
                                    - getCellRect(0, counter, true).x,
                            e.getY(), e.getClickCount(), e.isPopupTrigger());
                    JTreeTable.this.tree.dispatchEvent(newME);
                    processed = true;
                    break;
                }
            }
        }

        if (!processed) {
            super.processMouseEvent(e);
        }
    }

    @Override
    protected void processMouseMotionEvent(MouseEvent e) {
        boolean processed = false;

        // svs: pass mouse event to the tree, instead of editor
        for (int counter = getColumnCount() - 1; counter >= 0; counter--) {
            if (getColumnClass(counter) == TreeTableModel.class) {

                Rectangle cell = getCellRect(0, counter, true);
                if (e.getX() >= cell.x && e.getX() <= (cell.x + cell.width)) {

                    MouseEvent newME = new MouseEvent(JTreeTable.this.tree,
                            e.getID(), e.getWhen(), e.getModifiers(), e.getX()
                                    - getCellRect(0, counter, true).x,
                            e.getY(), e.getClickCount(), e.isPopupTrigger());
                    JTreeTable.this.tree.dispatchEvent(newME);
                    processed = true;
                    break;
                }
            }
        }

        if (!processed) {
            super.processMouseMotionEvent(e);
        }
    };

    /**
     * Overridden to message super and forward the method to the tree. Since the
     * tree is not actually in the component hierarchy it will never receive
     * this unless we forward it in this manner.
     */
    public void updateUI() {
        super.updateUI();
        if (tree != null) {
            tree.updateUI();
            // Do this so that the editor is referencing the current renderer
            // from the tree. The renderer can potentially change each time
            // laf changes.
            // svs: no editor
            // setDefaultEditor(TreeTableModel.class, new
            // TreeTableCellEditor());
        }
        // Use the tree's default foreground and background colors in the
        // table.
        LookAndFeel.installColorsAndFont(this, "Tree.background",
                "Tree.foreground", "Tree.font");
    }

    /**
     * Workaround for BasicTableUI anomaly. Make sure the UI never tries to
     * resize the editor. The UI currently uses different techniques to paint
     * the renderers and editors; overriding setBounds() below is not the right
     * thing to do for an editor. Returning -1 for the editing row in this case,
     * ensures the editor is never painted.
     */
    public int getEditingRow() {
        return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1
                : editingRow;
    }

    /**
     * Returns the actual row that is editing as <code>getEditingRow</code> will
     * always return -1.
     */
    protected int realEditingRow() {
        return editingRow;
    }

    /**
     * This is overridden to invoke super's implementation, and then, if the
     * receiver is editing a Tree column, the editor's bounds is reset. The
     * reason we have to do this is because JTable doesn't think the table is
     * being edited, as <code>getEditingRow</code> returns -1, and therefore
     * doesn't automatically resize the editor for us.
     */
    public void sizeColumnsToFit(int resizingColumn) {
        super.sizeColumnsToFit(resizingColumn);
        if (getEditingColumn() != -1
                && getColumnClass(editingColumn) == TreeTableModel.class) {
            Rectangle cellRect = getCellRect(realEditingRow(),
                    getEditingColumn(), false);
            Component component = getEditorComponent();
            component.setBounds(cellRect);
            component.validate();
        }
    }

    /**
     * Overridden to pass the new rowHeight to the tree.
     */
    public void setRowHeight(int rowHeight) {
        super.setRowHeight(rowHeight);
        if (tree != null && tree.getRowHeight() != rowHeight) {
            tree.setRowHeight(getRowHeight());
        }
    }

    /**
     * Returns the tree that is being shared between the model.
     */
    public JTree getTree() {
        return tree;
    }

    /**
     * Overridden to invoke repaint for the particular location if the column
     * contains the tree. This is done as the tree editor does not fill the
     * bounds of the cell, we need the renderer to paint the tree in the
     * background, and then draw the editor over it.
     */
    public boolean editCellAt(int row, int column, EventObject e) {
        boolean retValue = super.editCellAt(row, column, e);
        if (retValue && getColumnClass(column) == TreeTableModel.class) {
            repaint(getCellRect(row, column, false));
        }
        return retValue;
    }

    /**
     * A TreeCellRenderer that displays a JTree.
     */
    public class TreeTableCellRenderer extends JTree implements
            TableCellRenderer {

        /**
         *
         */
        private static final long serialVersionUID = 1L;
        /** Last table/tree row asked to renderer. */
        protected int visibleRow;
        /**
         * Border to draw around the tree, if this is non-null, it will be
         * painted.
         */
        protected Border highlightBorder;

        public TreeTableCellRenderer(TreeModel model) {
            super(model);

            // getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        }

        /**
         * updateUI is overridden to set the colors of the Tree's renderer to
         * match that of the table.
         */
        public void updateUI() {
            super.updateUI();
            // Make the tree's cell renderer use the table's cell selection
            // colors.
            TreeCellRenderer tcr = getCellRenderer();
            if (tcr instanceof DefaultTreeCellRenderer) {
                DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr);
                // For 1.1 uncomment this, 1.2 has a bug that will cause an
                // exception to be thrown if the border selection color is
                // null.
                // dtcr.setBorderSelectionColor(null);
                dtcr.setTextSelectionColor(UIManager
                        .getColor("Table.selectionForeground"));
                dtcr.setBackgroundSelectionColor(UIManager
                        .getColor("Table.selectionBackground"));
            }
        }

        /**
         * Sets the row height of the tree, and forwards the row height to the
         * table.
         */
        public void setRowHeight(int rowHeight) {
            if (rowHeight > 0) {
                super.setRowHeight(rowHeight);
                if (JTreeTable.this != null
                        && JTreeTable.this.getRowHeight() != rowHeight) {
                    JTreeTable.this.setRowHeight(getRowHeight());
                }
            }
        }

        /**
         * This is overridden to set the height to match that of the JTable.
         */
        public void setBounds(int x, int y, int w, int h) {
            super.setBounds(x, 0, w, JTreeTable.this.getHeight());
        }

        /**
         * Sublcassed to translate the graphics such that the last visible row
         * will be drawn at 0,0.
         */
        public void paint(Graphics g) {
            g.translate(0, -visibleRow * getRowHeight());
            super.paint(g);
            // Draw the Table border if we have focus.
            if (highlightBorder != null) {
                highlightBorder.paintBorder(this, g, 0, visibleRow
                        * getRowHeight(), getWidth(), getRowHeight());
            }
        }

        /**
         * TreeCellRenderer method. Overridden to update the visible row.
         */
        public Component getTableCellRendererComponent(JTable table,
                Object value, boolean isSelected, boolean hasFocus, int row,
                int column) {
            Color background;
            Color foreground;

            if (isSelected) {
                background = table.getSelectionBackground();
                foreground = table.getSelectionForeground();
            } else {
                background = table.getBackground();
                foreground = table.getForeground();
            }
            highlightBorder = null;
            if (realEditingRow() == row && getEditingColumn() == column) {
                background = UIManager.getColor("Table.focusCellBackground");
                foreground = UIManager.getColor("Table.focusCellForeground");
            } else if (hasFocus) {
                highlightBorder = UIManager
                        .getBorder("Table.focusCellHighlightBorder");
                if (isCellEditable(row, column)) {
                    background = UIManager
                            .getColor("Table.focusCellBackground");
                    foreground = UIManager
                            .getColor("Table.focusCellForeground");
                }
            }

            visibleRow = row;
            setBackground(background);

            TreeCellRenderer tcr = getCellRenderer();
            if (tcr instanceof DefaultTreeCellRenderer) {
                DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr);
                if (isSelected) {
                    dtcr.setTextSelectionColor(foreground);
                    dtcr.setBackgroundSelectionColor(background);
                } else {
                    dtcr.setTextNonSelectionColor(foreground);
                    dtcr.setBackgroundNonSelectionColor(background);
                }
            }
            return this;
        }
    }

    /**
     * An editor that can be used to edit the tree column. This extends
     * DefaultCellEditor and uses a JTextField (actually, TreeTableTextField) to
     * perform the actual editing.
     * <p>
     * To support editing of the tree column we can not make the tree editable.
     * The reason this doesn't work is that you can not use the same component
     * for editing and renderering. The table may have the need to paint cells,
     * while a cell is being edited. If the same component were used for the
     * rendering and editing the component would be moved around, and the
     * contents would change. When editing, this is undesirable, the contents of
     * the text field must stay the same, including the caret blinking, and
     * selections persisting. For this reason the editing is done via a
     * TableCellEditor.
     * <p>
     * Another interesting thing to be aware of is how tree positions its render
     * and editor. The render/editor is responsible for drawing the icon
     * indicating the type of node (leaf, branch...). The tree is responsible
     * for drawing any other indicators, perhaps an additional +/- sign, or
     * lines connecting the various nodes. So, the renderer is positioned based
     * on depth. On the other hand, table always makes its editor fill the
     * contents of the cell. To get the allusion that the table cell editor is
     * part of the tree, we don't want the table cell editor to fill the cell
     * bounds. We want it to be placed in the same manner as tree places it
     * editor, and have table message the tree to paint any decorations the tree
     * wants. Then, we would only have to worry about the editing part. The
     * approach taken here is to determine where tree would place the editor,
     * and to override the <code>reshape</code> method in the JTextField
     * component to nudge the textfield to the location tree would place it.
     * Since JTreeTable will paint the tree behind the editor everything should
     * just work. So, that is what we are doing here. Determining of the icon
     * position will only work if the TreeCellRenderer is an instance of
     * DefaultTreeCellRenderer. If you need custom TreeCellRenderers, that don't
     * descend from DefaultTreeCellRenderer, and you want to support editing in
     * JTreeTable, you will have to do something similiar.
     */
    // public class TreeTableCellEditor extends DefaultCellEditor {
    // /**
    // *
    // */
    // private static final long serialVersionUID = 1L;
    //
    // public TreeTableCellEditor() {
    // super(new TreeTableTextField());
    // }
    //
    // /**
    // * Overridden to determine an offset that tree would place the
    // * editor at. The offset is determined from the
    // * <code>getRowBounds</code> JTree method, and additionally
    // * from the icon DefaultTreeCellRenderer will use.
    // * <p>The offset is then set on the TreeTableTextField component
    // * created in the constructor, and returned.
    // */
    // public Component getTableCellEditorComponent(JTable table,
    // Object value,
    // boolean isSelected,
    // int r, int c) {
    // Component component = super.getTableCellEditorComponent
    // (table, value, isSelected, r, c);
    // JTree t = getTree();
    // boolean rv = t.isRootVisible();
    // int offsetRow = rv ? r : r - 1;
    // Rectangle bounds = t.getRowBounds(offsetRow);
    // int offset = bounds.x;
    // TreeCellRenderer tcr = t.getCellRenderer();
    // if (tcr instanceof DefaultTreeCellRenderer) {
    // Object node = t.getPathForRow(offsetRow).
    // getLastPathComponent();
    // Icon icon;
    // if (t.getModel().isLeaf(node))
    // icon = ((DefaultTreeCellRenderer)tcr).getLeafIcon();
    // else if (tree.isExpanded(offsetRow))
    // icon = ((DefaultTreeCellRenderer)tcr).getOpenIcon();
    // else
    // icon = ((DefaultTreeCellRenderer)tcr).getClosedIcon();
    // if (icon != null) {
    // offset += ((DefaultTreeCellRenderer)tcr).getIconTextGap() +
    // icon.getIconWidth();
    // }
    // }
    // ((TreeTableTextField)getComponent()).offset = offset;
    // return component;
    // }
    //
    // /**
    // * This is overridden to forward the event to the tree. This will
    // * return true if the click count >= 3, or the event is null.
    // */
    //
    // public boolean isCellEditable(EventObject e) {
    // if (e instanceof MouseEvent) {
    // MouseEvent me = (MouseEvent)e;
    // // If the modifiers are not 0 (or the left mouse button),
    // // tree may try and toggle the selection, and table
    // // will then try and toggle, resulting in the
    // // selection remaining the same. To avoid this, we
    // // only dispatch when the modifiers are 0 (or the left mouse
    // // button).
    // if (me.getModifiers() == 0 ||
    // me.getModifiers() == InputEvent.BUTTON1_MASK) {
    // for (int counter = getColumnCount() - 1; counter >= 0;
    // counter--) {
    // if (getColumnClass(counter) == TreeTableModel.class) {
    // MouseEvent newME = new MouseEvent
    // (JTreeTable.this.tree, me.getID(),
    // me.getWhen(), me.getModifiers(),
    // me.getX() - getCellRect(0, counter, true).x,
    // me.getY(), me.getClickCount(),
    // me.isPopupTrigger());
    // JTreeTable.this.tree.dispatchEvent(newME);
    // break;
    // }
    // }
    // }
    // if (me.getClickCount() >= 3) {
    // return true;
    // }
    // return false;
    // }
    // if (e == null) {
    // return true;
    // }
    // return false;
    // }
    //
    // }

    /**
     * Component used by TreeTableCellEditor. The only thing this does is to
     * override the <code>reshape</code> method, and to ALWAYS make the x
     * location be <code>offset</code>.
     */
    static class TreeTableTextField extends JTextField {
        /**
         *
         */
        private static final long serialVersionUID = 1L;
        // public int offset;
        // private int offset;

        // @SuppressWarnings("deprecation")
        // public void reshape(int x, int y, int w, int h) {
        // //int newX = Math.max(x, offset);
        // //super.reshape(newX, y, w - (newX - x), h);
        //
        // super.reshape(x, y, w, h);
        // }
    }

    /**
     * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel to
     * listen for changes in the ListSelectionModel it maintains. Once a change
     * in the ListSelectionModel happens, the paths are updated in the
     * DefaultTreeSelectionModel.
     */
    class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
        /**
         *
         */
        private static final long serialVersionUID = 1L;
        /** Set to true when we are updating the ListSelectionModel. */
        protected boolean updatingListSelectionModel;

        public ListToTreeSelectionModelWrapper() {
            super();
            getListSelectionModel().addListSelectionListener(
                    createListSelectionListener());
        }

        /**
         * Returns the list selection model. ListToTreeSelectionModelWrapper
         * listens for changes to this model and updates the selected paths
         * accordingly.
         */
        ListSelectionModel getListSelectionModel() {
            return listSelectionModel;
        }

        /**
         * This is overridden to set <code>updatingListSelectionModel</code> and
         * message super. This is the only place DefaultTreeSelectionModel
         * alters the ListSelectionModel.
         */
        public void resetRowSelection() {
            if (!updatingListSelectionModel) {
                updatingListSelectionModel = true;
                try {
                    super.resetRowSelection();
                } finally {
                    updatingListSelectionModel = false;
                }
            }
            // Notice how we don't message super if
            // updatingListSelectionModel is true. If
            // updatingListSelectionModel is true, it implies the
            // ListSelectionModel has already been updated and the
            // paths are the only thing that needs to be updated.
        }

        /**
         * Creates and returns an instance of ListSelectionHandler.
         */
        protected ListSelectionListener createListSelectionListener() {
            return new ListSelectionHandler();
        }

        /**
         * If <code>updatingListSelectionModel</code> is false, this will reset
         * the selected paths from the selected rows in the list selection
         * model.
         */
        protected void updateSelectedPathsFromSelectedRows() {
            if (!updatingListSelectionModel) {
                updatingListSelectionModel = true;
                try {
                    // This is way expensive, ListSelectionModel needs an
                    // enumerator for iterating.
                    int min = listSelectionModel.getMinSelectionIndex();
                    int max = listSelectionModel.getMaxSelectionIndex();

                    clearSelection();
                    if (min != -1 && max != -1) {
                        for (int counter = min; counter <= max; counter++) {
                            if (listSelectionModel.isSelectedIndex(counter)) {
                                TreePath selPath = tree.getPathForRow(counter);

                                if (selPath != null) {
                                    addSelectionPath(selPath);
                                }
                            }
                        }
                    }
                } finally {
                    updatingListSelectionModel = false;
                }
            }
        }

        /**
         * Class responsible for calling updateSelectedPathsFromSelectedRows
         * when the selection of the list changse.
         */
        class ListSelectionHandler implements ListSelectionListener {
            public void valueChanged(ListSelectionEvent e) {
                updateSelectedPathsFromSelectedRows();
            }
        }
    }
}
